Ana içeriğe geç
  1. 100 Günde SwiftUI Notları/

60.Gün - SwiftUI JSON Custom Codable Key ve FriendFace Milestone Projesi

Custom Codable Keys #

JSON verilerimiz tasarladığımız türlerle eşleştiğinde, Codable mükemmel çalışır. Aslında, Codable uyumluluğunu eklemekten başka bir şey yapmamamız genellikle yeterlidir - Swift derleyicisi ihtiyacımız olan her şeyi otomatik olarak oluşturacaktır.

Ancak, çoğu zaman işler bu kadar kolay değildir ve daha karmaşık verilerle çalışmak için üç seçeneğimiz vardır:

  1. Swift’ten property isimlerini otomatik olarak dönüştürmesini istemek.
  2. Özel property ismi dönüşümleri oluşturmak.
  3. Tamamen özel encoding ve decoding oluşturmak.

Genel olarak, bu seçenekleri tercih sıralamanız gerekir, 1. seçenek en tercih edilebilir, 3. seçenek ise en az tercih edilebilir olandır.

İlk iki seçeneği tek tek inceleyelim. 3. seçeneği karşılaştırmalı olarak daha karmaşık olduğu için şimdilik bırakacağım!

Swift’ten property isimlerini otomatik olarak dönüştürmesini istemek, gelen JSON’daki property isimlendirme kuralları Swift kodumuzunkinden farklı olduğunda kullanışlıdır. Örneğin, JSON property isimleri snake case (ör. first_name) olabilirken, Swift kodumuzda camel case (ör. firstName) kullanıyor olabiliriz.

Codable bu iki biçim arasında çeviri yapabilir, bunun için keyDecodingStrategy adlı bir özellik ayarlamamız gerekir.

Bunu göstermek için, iki özelliğe sahip bir User struct’ı bulunuyor:

struct User: Codable {
    var firstName: String
    var lastName: String
}

O, Swift kodunda genellikle kullanılan, kelimelerin ilk harflerinin büyük yazılması uygulaması develer sırtındaki hörgüçleri andırdığı için “camel case” olarak adlandırılan isimlendirme kuralını kullanıyor.

Şimdi de aynı iki özelliğe sahip bir JSON veri parçası verelim:

let str = """
{
    "first_name": "Andrew",
    "last_name": "Glouberman"
}
"""

let data = Data(str.utf8)

Bu JSON verisi, property isimleri tüm harfleri küçük yazılmış ve kelimeler alt çizgilerle ayrılmış “snake case” isimlendirme kuralını kullanıyor.

Eğer bu JSON’u bir User örneğine çözmeye çalışırsak, işe yaramaz, çünkü iki property farklı isimlendirme stillerini kullanıyor:

do {
    let decoder = JSONDecoder()

    let user = try decoder.decode(User.self, from: data)
    print("Hi, I'm \(user.firstName) \(user.lastName)")
} catch {
    print("Whoops: \(error.localizedDescription)")
} 

Ancak, decode() metodunu çağırmadan önce key decoding stratejisini değiştirirsek, Swift’ten snake case’i camel case’e ve tam tersi dönüştürmesini isteyebiliriz. Böylece, çözümleme işlemi başarılı olacaktır:

do {
    let decoder = JSONDecoder()
    decoder.keyDecodingStrategy = .convertFromSnakeCase

    let user = try decoder.decode(User.self, from: data)
    print("Hi, I'm \(user.firstName) \(user.lastName)")
} catch {
    print("Whoops: \(error.localizedDescription)")
} 

Bu, snake_case’den camelCase’e ve tam tersi dönüştürme konusunda harika çalışıyor, ama property isimlerimiz tamamen farklı olursa ne yapacağız? İşte o zaman ikinci seçeneğe, yani özel property ismi dönüşümleri oluşturmaya ihtiyacımız var.

Örnek olarak, şu JSON’a bakalım:

let str = """
{
    "first": "Andrew",
    "last": "Glouberman"
}
"""

Burada da kullanıcının adı ve soyadı mevcut, ancak property isimleri bizim struct’ımızdakilerle hiç uyuşmuyor.

Codable konusunu incelerken, encode ve decode key’leri tanımlayan bir CodingKeys enum’u oluşturabileceğimizi söylemiştim. O sırada “bu enum geleneksel olarak CodingKeys olarak adlandırılır, sonuna S konulur, ama istersen farklı bir şey de koyabilirsin” demiştim. Ancak bu, tüm hikaye değil.

Aslında CodingKeys ismini kullanmamızın sebebi, bu adın özel güçleri olması: Eğer bir CodingKeys enum’u varsa, Swift otomatik olarak bir nesnenin encode ve decode edilmesi için nasıl davranması gerektiğini belirler, böylece biz özel Codable implementasyonları sağlamak zorunda kalmayız.

Bunu anlamak biraz zor olabilir, bu yüzden bir kod örneğiyle göstermek daha iyi olacak. User struct’ını şöyle değiştirmeyi deneyin:

struct User: Codable {
    enum ZZZCodingKeys: CodingKey {
        case firstName
    }

    var firstName: String
    var lastName: String
}

O kod şu an için derlenebilir, çünkü ZZZCodingKeys adı Swift için anlamsızdır - sadece iç içe geçmiş bir enum’dur. Ama eğer enum’u sadece CodingKeys olarak yeniden adlandırırsanız, kod artık derlenmeyecektir: Artık yalnızca firstName özelliğini kodlamayı ve çözmeyi söylüyoruz, bu da lastName özelliğini ayarlayan bir başlatıcının olmadığı anlamına gelir - ve bu kabul edilemez.

Bunun nedeni, CodingKeys‘in ikinci bir süper gücünün olmasıdır: Poperty’lere ham değer string’leri eklediğimizde, Swift bunları JSON property isimleri için kullanır. Yani, case isimleri Swift property isimlerimizle eşleşmeli ve case değerleri de JSON property isimleriyle eşleşmelidir.

O halde, örnek JSON’umuza geri dönelim:

let str = """
{
    "first": "Andrew",
    "last": "Glouberman"
}
"""

Bu durumda “first” ve “last” JSON property isimleri kullanılırken, User struct’ımız firstName ve lastName özelliklerini kullanıyor. İşte burada CodingKeys bize yardımcı olabilir: Özel bir Codable uyumluluğu yazmamıza gerek yok, çünkü Swift property isimlerimizi JSON property isimlerine eşleyecek kodlama anahtarlarını ekleyebiliriz, şöyle:

struct User: Codable {
    enum CodingKeys: String, CodingKey {
        case firstName = "first"
        case lastName = "last"
    }

    var firstName: String
    var lastName: String
}

Artık Swift’e JSON ve Swift isimlendirme arasındaki dönüşümü nasıl yapacağını özel olarak söylediğimize göre, keyDecodingStrategy kullanmamıza artık gerek kalmadı - sadece o enum’u eklemek yeterli.

Yani, özel Codable uyumluluğu oluşturmayı bilmeniz gerekir, ama bu diğer seçenekler mümkünse, genellikle en iyi uygulama onları kullanmaktır.

Tamamen Özel Codable Implementation #

Şimdiye kadar, Swift’in snake case ve camel case arasında nasıl eşleme yapabileceğini ve JSON’un bir ismi olup Swift’in tamamen farklı bir isim kullandığı durumlarda nasıl eşleme belirtebileceğimizi gördünüz.

Son seçenek, değişikliklerin daha büyük olduğu zamanlar için, mesela JSON verisi bir sayıyı bir string olarak sağlıyorsa. Ancak, SwiftData modellerinin Codable‘a nasıl uygun hale getirileceğini göreceğiniz gibi, bu aynı zamanda bunu istediğinizde de kullanışlıdır.

Öncelikle, sorunu gösteren yeni bir JSON deneyelim:

let str = """
{
    "first": "Andrew",
    "last": "Glouberman",
    "age": "13"
}
"""

Burada adın ve soyadın isimleri pek yardımcı olmayan isimlere sahip, ayrıca bir sayı da bir string içinde saklanıyor. Bir sunucudan gelen JSON verilerinin sorunlarını düzeltmek için çok az şey yapabiliriz, ancak kesinlikle onların gariplikleri de kodumuzu kirletmesini istemeyiz - bu bir tamsayı ve biz bunu Swift kodumuzda bir tamsayı olarak saklamak istiyoruz.

Bu nedenle, firstName ve lastName özelliklerini düzeltip, age‘i bir tamsayı olarak saklayacak şöyle bir User struct tanımlayabiliriz:

struct User: Codable {
    enum CodingKeys: String, CodingKey {
        case firstName = "first"
        case lastName = "last"
        case age
    }

    var firstName: String
    var lastName: String
    var age: Int
}

Ancak şimdi bir sorunumuz var: Swift, property isimlerini bizim için dönüştürebilir, ama farklı veri türlerini işleyemez.

Bu durumda, tamamen özel bir Codable implementation oluşturmamız gerekiyor. Bunun için User struct’ına iki şey eklememiz gerekir:

  1. Bir Decoder instance kabul eden ve property’leri oradan okumayı bilen yeni bir initializer.
  2. Bir Encoder instance kabul eden ve property’leri oraya yazmayı bilen yeni bir encode(to:) methodu.

İpucu: Swift burada Decoder ve Encoder kullanır, çünkü Swift nesnelerine veri dönüştürmenin birçok yolu var - JSON bunlardan sadece biri.

Her ikisi de oldukça fazla kod gerektirir, ancak Xcode bazen bize yardımcı olabilir. Bu durumda, her ikisinin de çalışmasını sağlayacak tüm kodu doldurması gerekir: Özelliklerin altına init yazın, ardından init(from decoder: Decoder) seçeneğini seçip Enter’a basın, sonra encode yazın ve encode(to encoder: Encoder) seçeneğini seçip Enter’a basın.

Tamamlanmış User struct’ı şöyle görünmeli:

struct User: Codable {
    enum CodingKeys: String, CodingKey {
        case firstName = "first"
        case lastName = "last"
        case age
    }

    var firstName: String
    var lastName: String
    var age: Int

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.firstName = try container.decode(String.self, forKey: .firstName)
        self.lastName = try container.decode(String.self, forKey: .lastName)
        self.age = try container.decode(Int.self, forKey: .age)
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(self.firstName, forKey: .firstName)
        try container.encode(self.lastName, forKey: .lastName)
        try container.encode(self.age, forKey: .age)
    }
}

İpucu: Eğer bu bir struct yerine bir sınıf olsaydı, yeni initializer’ın required olarak işaretlenmesi gerekirdi, böylece alt sınıflar bunu uygulamak zorunda kalırlardı.

Evet, oldukça fazla kod oldu ama gerçekten de sadece dört satır önemli: init(from:) metodundan iki satır ve encode(to:) metodundan iki satır.

Önemli olan ilk satır, initializer’dan gelen bu satırdır:

let container = try decoder.container(keyedBy: CodingKeys.self)

Bu kod CodingKeys kullanarak JSON dosyasından yüklenebilecek tüm olası keyleri okumaktadır. CodingKeys enumunda aranır, bu nedenle .firstName ve .age gibi şeylere başvurulabilir.

İkinci önemli satır da bu, yine initializer’dan:

self.firstName = try container.decode(String.self, forKey: .firstName)

Bu kod, JSON’dan .firstName anahtarına karşılık gelen bir string’i okur ve struct’ın firstName property’sine atar. Bu kısım biraz karışık olabilir çünkü firstName iki kez var, bu yüzden kodun ne yaptığını yeniden ifade edeyim: ‘JSON’da CodingKeys.firstName‘e karşılık gelen property’yi bul ve bunu kendi yerel firstName değerimize ata.’

Bu küçük adım önemlidir, çünkü CodingKeys.firstName aslında firstName değil, çünkü biz bunu JSON’umuza uyan şekilde yeniden adlandırdık. Yani, gerçekte bu satır ‘JSON’daki first özelliğini bul ve yapımızın firstName property’ye ata’ anlamına geliyor - otomatik yeniden adlandırmanın hala gerçekleştiğinden emin olmak için.

Yardımcı olması için, kodu şöyle okuyabileceğinizi hayal edin:

self.structFirstName = try container.decode(String.self, forKey: .jsonFirstName)

Bu, ilk iki ilginç kod satırı. İkinci iki satır, ilk ikisinin tersini etkili bir şekilde yapıyor. Bunlar encode(to:) yönteminde:

var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.firstName, forKey: .firstName)

Bu ilk satır, tüm CodingKeys değerlerimizi depolayabileceğimiz bir yer oluşturmak istediğimiz anlamına geliyor ve ikinci satır ise mevcut firstName özelliğini CodingKeys.firstName‘de belirtilene yazmaktadır - burada da otomatik yeniden adlandırmanın first‘e yapılması önemli.

Bu noktada, muhtemelen bu kodu asla hatırlayamayacağınızı merak ediyorsunuz, çünkü tahmin edilebilir bir şey değil. Bu yüzden, size en önemli ipucumu vereyim:

Özel bir Codable uygulama gerçekleştirmeniz gerektiğinde ve Xcode bunu sizin için oluşturamadığında, tek özellikli ve tek durumlu bir CodingKeys enumuna sahip yeni, basit bir yapı oluşturun, Xcode’un oluşturduğu o uygulamayı kullanarak kendi uygulamanızı oluşturun.

Bu, SwiftData ile çalışırken özellikle önemlidir, çünkü Codable desteği eklemek, özel bir uygulama oluşturmak anlamına gelir. Yukarıdaki tüm kodu hatırlamak can sıkıcı ve Xcode kesinlikle yardımcı olmayacaktır, bu yüzden Xcode’un oluşturabileceği bir Codable uygulaması için geçici bir yapı oluşturun, sonra da SwiftData model sınıfınızı Codable yapmak için onun yapısını kullanın.

Neyse, bu noktaya geldik çünkü bir dizeyi bir tamsayıya yüklemek denedik, bu da bize Xcode’un oluşturduğu kodda iki değişiklik yapma ihtiyacı getirdi. İlk olarak, bu kod satırı değiştirilmeli:

self.age = try container.decode(Int.self, forKey: .age)

Bu, age property’yi bir tamsayı olarak okumaya çalışır ve başarısız olur. Bunun yerine, bir string olarak okumamız, ardından bunu bir tamsayıya dönüştürmemiz veya dönüştürme başarısız olursa varsayılan bir değer sağlamamız gerekir. Kodu bununla değiştirin:

let stringAge = try container.decode(String.self, forKey: .age)
self.age = Int(stringAge) ?? 0

Değiştirilmesi gereken ikinci şey encode(to:) satırındadır, böylece herhangi bir JSON yazmamız gerekirse mevcut formatı koruruz. Burada, bu satırın değişmesi gerekiyor:

try container.encode(self.age, forKey: .age)

Bu bir tamsayı yazar, ancak bunun gibi bir dize yazması gerekir:

try container.encode(String(self.age), forKey: .age)

Custom implementation oluşturmanın çok fazla güçlük gibi göründüğünü biliyorum, ancak gördüğünüz gibi bize ne olacağı konusunda tam kontrol sağlıyor: yükleme ve kaydetme işlemimize her türlü mantığı ekleyebilir, adları değiştirebilir, türleri değiştirebilir, varsayılan değerler sağlayabilir ve daha fazlasını yapabiliriz.

FriendFace Milestone Projesi #

İşte sıfırdan bir uygulama oluşturmanın zamanı geldi ve bugün özellikle kapsamlı bir meydan okuma: göreviniz internetten bazı JSON’ları indirmek için URLSession kullanmak, bunları Swift türlerine dönüştürmek için Codable kullanmak, ardından kullanıcıya göstermek için NavigationStack, List ve daha fazlasını kullanmak.

İlk adımınız JSON’ı incelemek olmalıdır. Kullanmak istediğiniz URL şudur: https://www.hackingwithswift.com/samples/friendface.json - bu, örnek kullanıcılar için rastgele oluşturulmuş büyük bir veri koleksiyonudur.

Görebileceğiniz gibi, bir insan dizisi var ve her kişinin bir kimliği, adı, yaşı, e-posta adresi ve daha fazlası var. Ayrıca bir dizi etiket dizisi ve her birinin adı ve kimliği olan bir dizi arkadaşları var.

Bunu ne kadar uygulayacağınız size kalmış, ancak en azından şunları yapmalısınız:

  • Verileri alın ve User ve Friend yapılarına ayrıştırın.
  • Adları ve şu anda aktif olup olmadıkları gibi haklarında biraz bilgi içeren bir kullanıcı listesi görüntüleyin.
  • Bir kullanıcıya dokunulduğunda gösterilen, arkadaşlarının isimleri de dahil olmak üzere onlar hakkında daha fazla bilgi sunan bir ayrıntı görünümü oluşturun.
  • İndirmeye başlamadan önce, User dizinizin boş olduğunu kontrol edin, böylece görünüm her gösterildiğinde indirmeyi tekrar tekrar başlatmazsınız.

Nereden başlayacağınızdan emin değilseniz, türlerinizi tasarlayarak başlayın: name, age, company ve benzeri özelliklerle bir User yapısı, ardından id ve name ile bir Friend yapısı oluşturun. Bundan sonra, verileri almak ve türlerinize çözmek için bazı URLSession kodlarına geçin.

Her kullanıcının kayıt olduğu tarihin çok özel bir biçimi olduğunu fark edebilirsiniz: 2015-11-10T01:47:18-00:00. Bu ISO-8601 olarak bilinir ve o kadar yaygındır ki, otomatik olarak çözen .iso8601 adında yerleşik bir dateDecodingStrategy vardır.

Bunu oluştururken, aklınızda bir şey tutmanızı istiyorum: bu tür bir uygulama iOS uygulama geliştirmenin olmazsa olmazıdır - eğer bunu güvenle yapabilirseniz, tam zamanlı bir uygulama geliştirici olma yolunda iyi ilerliyorsunuz demektir.

İpucu: Her zaman olduğu gibi, bu meydan okumayı çözmenin en iyi yolu basit tutmaktır - meydan okumayı çözmek için mümkün olduğunca az kod yazın ve iyi çalıştığından emin olun.

Çözüm #

Bu projeye ait çözüm aşağıdaki GitHub adresinde bulunmaktadır;

https://github.com/GorkemGuray/FriendFace


Bu yazıyı İngilizce olarak da okuyabilirsiniz.
You can also read this article in English.

Bu yazı, SwiftUI Day 60 adresinde bulunan yazılardan kendim için aldığım notları içermektedir. Orjinal dersi takip etmek için lütfen bağlantıya tıklayın.

Görkem Güray
Yazar
Görkem Güray
Makine imalatı yapan bir firmada Endüstriyel Otomasyon Mühendisi olarak çalışıyorum. Genellikle Omron sistemler ile makine yazılımları geliştiriyorum.

comments powered by Disqus Mastodon